using AtomUI.Utils;
using Avalonia;
using Avalonia.Media;
using Avalonia.Utilities;

namespace AtomUI.Controls.Utils;

/// <summary>
/// Contains internal helpers used to build and draw various geometries.
/// </summary>
internal class RoundRectGeometryBuilder
{
   private const double PiOver2 = 1.57079633; // 90 deg to rad
   private const double Epsilon = 0.00000153; // Same as LayoutHelper.LayoutEpsilon

   /// <summary>
   /// Draws a new rounded rectangle within the given geometry context.
   /// Warning: The caller must manage and dispose the <see cref="StreamGeometryContext"/> externally.
   /// </summary>
   /// <remarks>
   /// WinUI: https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L1072-L1079
   /// </remarks>
   /// <param name="context">The geometry context to draw into.</param>
   /// <param name="keypoints">The rounded rectangle keypoints defining the rectangle to draw.</param>
   public static void DrawRoundedCornersRectangle(
      StreamGeometryContext context,
      ref RoundedRectKeypoints keypoints)
   {
      double radiusX;
      double radiusY;

      context.BeginFigure(keypoints.TopLeft, isFilled: true);

      // Top
      context.LineTo(keypoints.TopRight);

      // TopRight corner
      radiusX = keypoints.RightTop.X - keypoints.TopRight.X;
      radiusY = keypoints.TopRight.Y - keypoints.RightTop.Y;
      radiusX = radiusX > 0 ? radiusX : -radiusX;
      radiusY = radiusY > 0 ? radiusY : -radiusY;

      context.ArcTo(
         keypoints.RightTop,
         new Size(radiusX, radiusY),
         rotationAngle: 0.0,
         isLargeArc: false,
         SweepDirection.Clockwise);

      // Right
      context.LineTo(keypoints.RightBottom);

      // BottomRight corner
      radiusX = keypoints.RightBottom.X - keypoints.BottomRight.X;
      radiusY = keypoints.BottomRight.Y - keypoints.RightBottom.Y;
      radiusX = radiusX > 0 ? radiusX : -radiusX;
      radiusY = radiusY > 0 ? radiusY : -radiusY;

      if (radiusX != 0 || radiusY != 0) {
         context.ArcTo(
            keypoints.BottomRight,
            new Size(radiusX, radiusY),
            rotationAngle: 0.0,
            isLargeArc: false,
            SweepDirection.Clockwise);
      }

      // Bottom
      context.LineTo(keypoints.BottomLeft);

      // BottomLeft corner
      radiusX = keypoints.BottomLeft.X - keypoints.LeftBottom.X;
      radiusY = keypoints.BottomLeft.Y - keypoints.LeftBottom.Y;
      radiusX = radiusX > 0 ? radiusX : -radiusX;
      radiusY = radiusY > 0 ? radiusY : -radiusY;

      if (radiusX != 0 || radiusY != 0) {
         context.ArcTo(
            keypoints.LeftBottom,
            new Size(radiusX, radiusY),
            rotationAngle: 0.0,
            isLargeArc: false,
            SweepDirection.Clockwise);
      }

      // Left
      context.LineTo(keypoints.LeftTop);

      // TopLeft corner
      radiusX = keypoints.TopLeft.X - keypoints.LeftTop.X;
      radiusY = keypoints.TopLeft.Y - keypoints.LeftTop.Y;
      radiusX = radiusX > 0 ? radiusX : -radiusX;
      radiusY = radiusY > 0 ? radiusY : -radiusY;

      if (radiusX != 0 || radiusY != 0) {
         context.ArcTo(
            keypoints.TopLeft,
            new Size(radiusX, radiusY),
            rotationAngle: 0.0,
            isLargeArc: false,
            SweepDirection.Clockwise);
      }

      context.EndFigure(isClosed: true);
   }

   /// <summary>
   /// Draws a new rounded rectangle within the given geometry context.
   /// Warning: The caller must manage and dispose the <see cref="StreamGeometryContext"/> externally.
   /// </summary>
   /// <param name="context">The geometry context to draw into.</param>
   /// <param name="rect">The existing rectangle dimensions without corner radii.</param>
   /// <param name="radiusX">The radius on the X-axis used to round the corners of the rectangle.</param>
   /// <param name="radiusY">The radius on the Y-axis used to round the corners of the rectangle.</param>
   public static void DrawRoundedCornersRectangle(
      StreamGeometryContext context,
      Rect rect,
      double radiusX,
      double radiusY)
   {
      var arcSize = new Size(radiusX, radiusY);

      // The rectangle is constructed as follows:
      //
      //   (origin)
      //   Corner 4            Corner 1
      //   Top/Left  Line 1    Top/Right
      //      \_   __________   _/
      //          |          |
      //   Line 4 |          | Line 2
      //       _  |__________|  _
      //      /      Line 3      \
      //   Corner 3            Corner 2
      //   Bottom/Left         Bottom/Right
      //
      // - Lines 1,3 follow the deflated rectangle bounds minus RadiusX
      // - Lines 2,4 follow the deflated rectangle bounds minus RadiusY
      // - All corners are constructed using elliptical arcs 

      context.BeginFigure(new Point(rect.Left + radiusX, rect.Top), isFilled: true);

      // Line 1 + Corner 1
      context.LineTo(new Point(rect.Right - radiusX, rect.Top));
      context.ArcTo(
         new Point(rect.Right, rect.Top + radiusY),
         arcSize,
         rotationAngle: PiOver2,
         isLargeArc: false,
         SweepDirection.Clockwise);

      // Line 2 + Corner 2
      context.LineTo(new Point(rect.Right, rect.Bottom - radiusY));
      context.ArcTo(
         new Point(rect.Right - radiusX, rect.Bottom),
         arcSize,
         rotationAngle: PiOver2,
         isLargeArc: false,
         SweepDirection.Clockwise);

      // Line 3 + Corner 3
      context.LineTo(new Point(rect.Left + radiusX, rect.Bottom));
      context.ArcTo(
         new Point(rect.Left, rect.Bottom - radiusY),
         arcSize,
         rotationAngle: PiOver2,
         isLargeArc: false,
         SweepDirection.Clockwise);

      // Line 4 + Corner 4
      context.LineTo(new Point(rect.Left, rect.Top + radiusY));
      context.ArcTo(
         new Point(rect.Left + radiusX, rect.Top),
         arcSize,
         rotationAngle: PiOver2,
         isLargeArc: false,
         SweepDirection.Clockwise);

      context.EndFigure(isClosed: true);
   }

   /// <summary>
   /// Calculates the keypoints of a rounded rectangle based on the algorithm in WinUI.
   /// These keypoints may then be drawn or transformed into other types.
   /// </summary>
   /// <param name="outerBounds">The outer bounds of the rounded rectangle.
   /// This should be the overall bounds and size of the shape/control without any
   /// corner radii or border thickness adjustments.</param>
   /// <param name="borderThickness">The unadjusted border thickness of the rounded rectangle.</param>
   /// <param name="cornerRadius">The unadjusted corner radii of the rounded rectangle.
   /// The corner radius is defined to be the middle of the border stroke (center of the border).</param>
   /// <param name="sizing">The sizing mode used to calculate the final rounded rectangle size.</param>
   /// <returns>New rounded rectangle keypoints.</returns>
   public static RoundedRectKeypoints CalculateRoundedCornersRectangleWinUI(
      Rect outerBounds,
      Thickness borderThickness,
      CornerRadius cornerRadius,
      BackgroundSizing sizing)
   {
      // This was initially derived from WinUI:
      //  - CGeometryBuilder::CalculateRoundedCornersRectangle
      //    https://github.com/microsoft/microsoft-ui-xaml/blob/93742a178db8f625ba9299f62c21f656e0b195ad/dxaml/xcp/core/core/elements/geometry.cpp#L862-L869
      //
      // It has been modified to accept a BackgroundSizing parameter directly as well
      // as to support BackgroundSizing.CenterBorder.
      //
      // Keep in mind:
      //   > In Xaml, the corner radius is defined to be the middle of the stroke
      //   > (i.e. half the border thickness extends to either side).

      bool fOuter;
      Rect boundRect = outerBounds;

      if (sizing == BackgroundSizing.InnerBorderEdge) {
         boundRect = outerBounds.Deflate(borderThickness);
         fOuter = false;
      } else if (sizing == BackgroundSizing.OuterBorderEdge) {
         fOuter = true;
      } else // CenterBorder
      {
         // This is a trick to support a 3rd state (CenterBorder) using the same WinUI-based algorithm.
         // The WinUI algorithm only supports the fOuter = True|False parameter.
         boundRect = outerBounds.Deflate(borderThickness * 0.5);
         fOuter = false;
      }

      // Start of WinUI converted code
      // WinUI's Point struct fields can be modified directly, Avalonia's Point is read-only.
      // Therefore, we will use doubles for calculation so multiple Point structs aren't
      // required during calculations -- everything can be done with these double variables.
      double fLeftTop;
      double fLeftBottom;
      double fTopLeft;
      double fTopRight;
      double fRightTop;
      double fRightBottom;
      double fBottomLeft;
      double fBottomRight;

      double left;
      double right;
      double top;
      double bottom;

      // If the caller wants to take the border into account
      // initialize the borders variables
      if (borderThickness != default) {
         left = 0.5 * borderThickness.Left;
         right = 0.5 * borderThickness.Right;
         top = 0.5 * borderThickness.Top;
         bottom = 0.5 * borderThickness.Bottom;
      } else {
         left = 0.0;
         right = 0.0;
         top = 0.0;
         bottom = 0.0;
      }

      // The following if/else block initializes the variables
      // of which the points of the path will be created
      // In case of outer, add the border - if any.
      // Otherwise (inner rectangle) subtract the border - if any
      if (fOuter) {
         if (MathUtilities.AreClose(cornerRadius.TopLeft, 0.0, Epsilon)) {
            fLeftTop = 0.0;
            fTopLeft = 0.0;
         } else {
            fLeftTop = cornerRadius.TopLeft + left;
            fTopLeft = cornerRadius.TopLeft + top;
         }

         if (MathUtilities.AreClose(cornerRadius.TopRight, 0.0, Epsilon)) {
            fTopRight = 0.0;
            fRightTop = 0.0;
         } else {
            fTopRight = cornerRadius.TopRight + top;
            fRightTop = cornerRadius.TopRight + right;
         }

         if (MathUtilities.AreClose(cornerRadius.BottomRight, 0.0, Epsilon)) {
            fRightBottom = 0.0;
            fBottomRight = 0.0;
         } else {
            fRightBottom = cornerRadius.BottomRight + right;
            fBottomRight = cornerRadius.BottomRight + bottom;
         }

         if (MathUtilities.AreClose(cornerRadius.BottomLeft, 0.0, Epsilon)) {
            fBottomLeft = 0.0;
            fLeftBottom = 0.0;
         } else {
            fBottomLeft = cornerRadius.BottomLeft + bottom;
            fLeftBottom = cornerRadius.BottomLeft + left;
         }
      } else {
         fLeftTop = Math.Max(0.0, cornerRadius.TopLeft - left);
         fTopLeft = Math.Max(0.0, cornerRadius.TopLeft - top);
         fTopRight = Math.Max(0.0, cornerRadius.TopRight - top);
         fRightTop = Math.Max(0.0, cornerRadius.TopRight - right);
         fRightBottom = Math.Max(0.0, cornerRadius.BottomRight - right);
         fBottomRight = Math.Max(0.0, cornerRadius.BottomRight - bottom);
         fBottomLeft = Math.Max(0.0, cornerRadius.BottomLeft - bottom);
         fLeftBottom = Math.Max(0.0, cornerRadius.BottomLeft - left);
      }

      double topLeftX = fLeftTop;
      double topLeftY = 0;

      double topRightX = boundRect.Width - fRightTop;
      double topRightY = 0;

      double rightTopX = boundRect.Width;
      double rightTopY = fTopRight;

      double rightBottomX = boundRect.Width;
      double rightBottomY = boundRect.Height - fBottomRight;

      double bottomRightX = boundRect.Width - fRightBottom;
      double bottomRightY = boundRect.Height;

      double bottomLeftX = fLeftBottom;
      double bottomLeftY = boundRect.Height;

      double leftBottomX = 0;
      double leftBottomY = boundRect.Height - fBottomLeft;

      double leftTopX = 0;
      double leftTopY = fTopLeft;

      // check keypoints for overlap and resolve by partitioning radii according to
      // the percentage of each one.

      // top edge
      if (topLeftX > topRightX) {
         double v = (fLeftTop) / (fLeftTop + fRightTop) * boundRect.Width;
         topLeftX = v;
         topRightX = v;
      }

      // right edge
      if (rightTopY > rightBottomY) {
         double v = (fTopRight) / (fTopRight + fBottomRight) * boundRect.Height;
         rightTopY = v;
         rightBottomY = v;
      }

      // bottom edge
      if (bottomRightX < bottomLeftX) {
         double v = (fLeftBottom) / (fLeftBottom + fRightBottom) * boundRect.Width;
         bottomRightX = v;
         bottomLeftX = v;
      }

      // left edge
      if (leftBottomY < leftTopY) {
         double v = (fTopLeft) / (fTopLeft + fBottomLeft) * boundRect.Height;
         leftBottomY = v;
         leftTopY = v;
      }

      // The above code does all calculations without taking into consideration X/Y absolute position.
      // In WinUI, this is compensated for in DrawRoundedCornersRectangle(); however, we do this here directly
      // when the final keypoints are being created.
      var keypoints = new RoundedRectKeypoints();
      keypoints.TopLeft = new Point(
         boundRect.X + topLeftX,
         boundRect.Y + topLeftY);
      keypoints.TopRight = new Point(
         boundRect.X + topRightX,
         boundRect.Y + topRightY);

      keypoints.RightTop = new Point(
         boundRect.X + rightTopX,
         boundRect.Y + rightTopY);
      keypoints.RightBottom = new Point(
         boundRect.X + rightBottomX,
         boundRect.Y + rightBottomY);

      keypoints.BottomRight = new Point(
         boundRect.X + bottomRightX,
         boundRect.Y + bottomRightY);
      keypoints.BottomLeft = new Point(
         boundRect.X + bottomLeftX,
         boundRect.Y + bottomLeftY);

      keypoints.LeftBottom = new Point(
         boundRect.X + leftBottomX,
         boundRect.Y + leftBottomY);
      keypoints.LeftTop = new Point(
         boundRect.X + leftTopX,
         boundRect.Y + leftTopY);

      return keypoints;
   }

   /// <summary>
   /// Represents the keypoints of a rounded rectangle.
   /// These keypoints can be shared between methods and turned into geometry.
   /// </summary>
   /// <remarks>
   /// A rounded rectangle is the base geometric shape used when drawing borders.
   /// It is a superset of a simple rectangle (which has corner radii set to zero).
   /// These keypoints can be combined together to produce geometries for both background
   /// and border elements.
   /// </remarks>
   internal struct RoundedRectKeypoints
   {
      // The following keypoints are defined for a rounded rectangle:
      //
      //       TopLeft                                  TopRight
      //              *--------------------------------*
      // (start)     /                                  \
      //    LeftTop *                                    * RightTop
      //            |                                    |
      //            |                                    |
      // LeftBottom *                                    * RightBottom
      //             \                                  /
      //              *--------------------------------*
      //    BottomLeft                                  BottomRight
      //
      // Or, for a simple rectangle without corner radii:
      //
      //    TopLeft = LeftTop                   TopRight = RightTop
      //  (start)   *------------------------------------*
      //            |                                    |
      //            |                                    |
      //            *------------------------------------*
      // BottomLeft = LeftBottom             BottomRight = RightBottom

      /// <summary>
      /// Initializes a new instance of the <see cref="RoundedRectKeypoints"/> struct.
      /// </summary>
      public RoundedRectKeypoints()
      {
      }

      /// <summary>
      /// Initializes a new instance of the <see cref="RoundedRectKeypoints"/> struct.
      /// </summary>
      /// <param name="roundedRect">An existing <see cref="RoundedRect"/> to initialize keypoints with.</param>
      public RoundedRectKeypoints(RoundedRect roundedRect)
      {
         LeftTop = new Point(
            roundedRect.Rect.TopLeft.X,
            roundedRect.Rect.TopLeft.Y + roundedRect.RadiiTopLeft.Y);
         TopLeft = new Point(
            roundedRect.Rect.TopLeft.X + roundedRect.RadiiTopLeft.X,
            roundedRect.Rect.TopLeft.Y);
         TopRight = new Point(
            roundedRect.Rect.TopRight.X - roundedRect.RadiiTopRight.X,
            roundedRect.Rect.TopRight.Y);
         RightTop = new Point(
            roundedRect.Rect.TopRight.X,
            roundedRect.Rect.TopRight.Y + roundedRect.RadiiTopRight.Y);
         RightBottom = new Point(
            roundedRect.Rect.BottomRight.X,
            roundedRect.Rect.BottomRight.Y - roundedRect.RadiiBottomRight.Y);
         BottomRight = new Point(
            roundedRect.Rect.BottomRight.X - roundedRect.RadiiBottomRight.X,
            roundedRect.Rect.BottomRight.Y);
         BottomLeft = new Point(
            roundedRect.Rect.BottomLeft.X + roundedRect.RadiiBottomLeft.X,
            roundedRect.Rect.BottomLeft.Y);
         LeftBottom = new Point(
            roundedRect.Rect.BottomLeft.X,
            roundedRect.Rect.BottomRight.Y - roundedRect.RadiiBottomLeft.Y);
      }

      /// <summary>
      /// Gets the topmost point in the left line segment of the rectangle.
      /// </summary>
      public Point LeftTop { get; set; }

      /// <summary>
      /// Gets the leftmost point in the top line segment of the rectangle.
      /// </summary>
      public Point TopLeft { get; set; }

      /// <summary>
      /// Gets the rightmost point in the top line segment of the rectangle.
      /// </summary>
      public Point TopRight { get; set; }

      /// <summary>
      /// Gets the topmost point in the right line segment of the rectangle.
      /// </summary>
      public Point RightTop { get; set; }

      /// <summary>
      /// Gets the bottommost point in the right line segment of the rectangle.
      /// </summary>
      public Point RightBottom { get; set; }

      /// <summary>
      /// Gets the rightmost point in the bottom line segment of the rectangle.
      /// </summary>
      public Point BottomRight { get; set; }

      /// <summary>
      /// Gets the leftmost point in the bottom line segment of the rectangle.
      /// </summary>
      public Point BottomLeft { get; set; }

      /// <summary>
      /// Gets the bottommost point in the left line segment of the rectangle.
      /// </summary>
      public Point LeftBottom { get; set; }

      /// <summary>
      /// Gets a value indicating whether the rounded rectangle is actually rounded on
      /// any corner. If false the key points represent a simple rectangle.
      /// </summary>
      public bool IsRounded
      {
         get
         {
            return (TopLeft != LeftTop ||
                    TopRight != RightTop ||
                    BottomLeft != LeftBottom ||
                    BottomRight != RightBottom);
         }
      }

      /// <summary>
      /// Converts the keypoints into a simple rectangle (with no corners).
      /// This is equivalent to the outer rectangle with zero corner radii.
      /// </summary>
      /// <remarks>
      /// Warning: This will force the keypoints into a simple rectangle without
      /// any rounded corners. Use <see cref="IsRounded"/> to determine if corner
      /// information is otherwise available.
      /// </remarks>
      /// <returns>A new rectangle representing the keypoints.</returns>
      public Rect ToRect()
      {
         return new Rect(
            topLeft: new Point(
               x: LeftTop.X,
               y: TopLeft.Y),
            bottomRight: new Point(
               x: RightBottom.X,
               y: BottomRight.Y));
      }

      /// <summary>
      /// Converts the keypoints into a rounded rectangle with elliptical corner radii.
      /// </summary>
      /// <remarks>
      /// Elliptical corner radius (represented by <see cref="Vector"/>) is more powerful
      /// than circular corner radius (represented by a <see cref="CornerRadius"/>).
      /// Elliptical is a superset of circular.
      /// </remarks>
      /// <returns>A new rounded rectangle representing the keypoints.</returns>
      public RoundedRect ToRoundedRect()
      {
         return new RoundedRect(
            ToRect(),
            radiiTopLeft: new Vector(
               x: TopLeft.X - LeftTop.X,
               y: LeftTop.Y - TopLeft.Y),
            radiiTopRight: new Vector(
               x: RightTop.X - TopRight.X,
               y: RightTop.Y - TopRight.Y),
            radiiBottomRight: new Vector(
               x: RightBottom.X - BottomRight.X,
               y: BottomRight.Y - RightBottom.Y),
            radiiBottomLeft: new Vector(
               x: BottomLeft.X - LeftBottom.X,
               y: BottomLeft.Y - LeftBottom.Y));
      }
   }
}